﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Markup;
using System.Reflection;
using System.Windows;
using System.Collections;
using System.Globalization;

namespace PasswordProvider.View.Behaviors
{
    /// <summary>
    /// Enables binding a property value whether or not the property is a DependencyProperty 
    /// and whether or not the target object is in the visual or logical tree referencing the 
    /// data context which contains the binding value. For example, the InputBindings section 
    /// of a Window does not inherit the Window's DataContext, this extension find's the owing
    /// element's root DataContext (using reflection for accessing private fields to do so) 
    /// and uses it as the sorce for the binding. 
    /// 
    /// Based on March 17, 2009 — Thomas Levesque 
    /// http://tomlev2.wordpress.com/?s=CommandBindingExtension
    /// 
    /// I've modified Levesque's code just enough to generalize the target type, plus extended the 
    /// support of binding path to include '.' separated property accessors and indexed properties.    
    /// </summary>
    internal class ObjectBindingExtension : MarkupExtension
    {
        public ObjectBindingExtension () { }
        public ObjectBindingExtension(string bindingPath)
        {
            BindingPath = bindingPath;
        }

        [ConstructorArgument("bindingPath")]
        public string BindingPath { get; set; }

        private object _targetObject;
        private object _targetProperty;
        private DependencyPropertyChangedEventHandler _dataContextChangedHandler;

        /// <summary>
        /// Marked as sealed to make it impossible to override or hide.
        /// </summary>
        public sealed override object ProvideValue(IServiceProvider serviceProvider)
        {
            IProvideValueTarget provideValueTarget = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
            if (provideValueTarget == null)
                return null; // never reached?

            _targetObject = provideValueTarget.TargetObject;
            _targetProperty = provideValueTarget.TargetProperty;

            if (!string.IsNullOrEmpty(BindingPath))
            {
                // The serviceProvider is actually a ProvideValueServiceProvider, which has a private field "_context" of type ParserContext
                ParserContext parserContext = GetPrivateFieldValue<ParserContext>(serviceProvider, "_context");
                if (parserContext != null)
                {
                    // A ParserContext has a private field "_rootElement", which returns the root element of the XAML file
                    FrameworkElement rootElement = GetPrivateFieldValue<FrameworkElement>(parserContext, "_rootElement");
                    if (rootElement != null)
                    {
                        // Now we can retrieve the DataContext
                        object dataContext = rootElement.DataContext;

                        // This is the tricky part, if the target object is a SharedDp, it means that it is defined inside a template, 
                        // and the templated object itself has not yet been instanciated (is in the process of being instanciated?)
                        // Return a something that can be assigned to the target and the framework will end up updating
                        // in some cases.
                        if (dataContext == null && _targetObject.GetType().FullName == "System.Windows.SharedDp")
                        {
                            // return here if the data context is null, since there's no 
                            // sense in setting an event handler when we can't set the value on a SharedDP
                            return DummyValue;
                        }

                        // The DataContext may not be set yet when the FrameworkElement is first created, and it may change afterwards,
                        // so we handle the DataContextChanged event to update the Command when needed
                        if (_dataContextChangedHandler == null)
                        {
                            _dataContextChangedHandler = new DependencyPropertyChangedEventHandler(RootElement_DataContextChanged);
                            rootElement.DataContextChanged += _dataContextChangedHandler;
                        }

                        // If the data context is not null, we can simply get the value and return it.
                        if (dataContext != null)
                        {
                            object value = GetValue(dataContext, BindingPath);
                            if (value != null)
                                return value;
                        }
                    }
                }
                else
                {
                }
            }
            return DummyValue;
        }

        /// <summary>
        /// "Dummy" value to return when the data context hasn't been set yet and the target type is SharedDP.
        /// This is the only method inheritors should need to override. Return a dummy value of the type expected
        /// by the property you are targeting. 
        /// </summary>
        public virtual object DummyValue
        {
            get
            {
                if (_dummyValue == null)
                    _dummyValue = new object();
                return _dummyValue;
            }
        } private static object _dummyValue;

        private static object GetValue(object dataContext, string valuePath)
        {
            // DW: I edited this method to allow for extended path specification of command.
            // Such as Property.SubProperty.Command as well as indexed properties.

            string[] commandPath = valuePath.Split('.');

            object target = dataContext;
            for (int i = 0; i < commandPath.Length; i++)
            {
                string propertyName = commandPath[i];
                int index = -1;
                if (propertyName[propertyName.Length - 1] == ']')
                {
                    int pos = propertyName.LastIndexOf('[');
                    index = Int32.Parse(propertyName.Substring(pos+1, propertyName.Length - (pos+2)), CultureInfo.InvariantCulture);
                    propertyName = propertyName.Remove(pos);
                }
                
                PropertyInfo prop = target.GetType().GetProperty(propertyName);
                
                if (prop != null)
                {
                    target = prop.GetValue(target, null);
                    if (index >= 0)
                    {
                        IList list = target as IList;
                        if (list != null)
                        {
                            target = list[index];
                        }
                    }
                }
            }

            return target;

        }

        private void AssignValue(object value)
        {
            if (_targetObject != null && _targetProperty != null)
            {
                if (_targetProperty is DependencyProperty && _targetObject is DependencyObject)
                {
                    DependencyObject depObj = _targetObject as DependencyObject;
                    DependencyProperty depProp = _targetProperty as DependencyProperty;
                    depObj.SetValue(depProp, value);
                }

                else if(_targetProperty is PropertyInfo)
                {
                    PropertyInfo prop = _targetProperty as PropertyInfo;
                    prop.SetValue(_targetObject, value, null);
                }
            }
        }

        protected virtual void RootElement_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            FrameworkElement rootElement = sender as FrameworkElement;
            if (rootElement != null)
            {
                // TODO: Would leaving the handler in place cause this object to persist longer than necessary?
                // Doing so means that the bound value won't be updated if
                // the data context changes. 
                //rootElement.DataContextChanged -= _dataContextChangedHandler;

                object dataContext = rootElement.DataContext;
                if (dataContext != null)
                {
                    object value = GetValue(dataContext, BindingPath);
                    if (value != null)
                    {
                        AssignValue(value);
                    }
                }
            }
        }

        private static T GetPrivateFieldValue<T>(object target, string fieldName)
        {
            FieldInfo field = target.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
            if (field != null)
            {
                return (T)field.GetValue(target);
            }
            return default(T);
        }

    }
}
